package jmine.tec.utils.collection.impl;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Versao de HashMap que faz copias durante escritas, evitando sincronizar o mapa para leituras. Metodos de escrita sao sincronizados. Os
 * metodos que devolvem uma view do mapa (como {@link #keySet()}) devolvem uma view do mapa do momento em que foram criadas e nao sao
 * modificadas, mesmo com a modificacao do mapa original. As colecoes devolvidas tambem nao podem ser modificadas.
 * 
 * @author takeshi
 * @param <K> o tipo da chave
 * @param <V> o tipo do valor
 */
public class CopyOnWriteHashMap<K, V> implements ConcurrentMap<K, V> {

    private final AtomicReference<Map<K, V>> reference;

    /**
     * C'tor
     */
    public CopyOnWriteHashMap() {
        reference = new AtomicReference<Map<K, V>>(new HashMap<K, V>());
    }

    /**
     * C'tor
     * 
     * @param map {@link Map}
     */
    public CopyOnWriteHashMap(Map<? extends K, ? extends V> map) {
        reference = new AtomicReference<Map<K, V>>(new HashMap<K, V>(map));
    }

    /**
     * {@inheritDoc}
     * 
     * @see java.util.Map#clear()
     */
    public void clear() {
        reference.set(new HashMap<K, V>());
    }

    /**
     * {@inheritDoc}
     * 
     * @see java.util.Map#containsKey(java.lang.Object)
     */
    public boolean containsKey(Object key) {
        return this.reference.get().containsKey(key);
    }

    /**
     * {@inheritDoc}
     * 
     * @see java.util.Map#containsValue(java.lang.Object)
     */
    public boolean containsValue(Object value) {
        return this.reference.get().containsValue(value);
    }

    /**
     * {@inheritDoc}
     * 
     * @see java.util.Map#entrySet()
     */
    public Set<java.util.Map.Entry<K, V>> entrySet() {
        return Collections.unmodifiableSet(this.reference.get().entrySet());
    }

    /**
     * {@inheritDoc}
     * 
     * @see java.util.Map#get(java.lang.Object)
     */
    public V get(Object key) {
        return this.reference.get().get(key);
    }

    /**
     * {@inheritDoc}
     * 
     * @see java.util.Map#isEmpty()
     */
    public boolean isEmpty() {
        return this.reference.get().isEmpty();
    }

    /**
     * {@inheritDoc}
     * 
     * @see java.util.Map#keySet()
     */
    public Set<K> keySet() {
        return Collections.unmodifiableSet(this.reference.get().keySet());
    }

    /**
     * {@inheritDoc}
     * 
     * @see java.util.Map#put(java.lang.Object, java.lang.Object)
     */
    public V put(K key, V value) {
        while (true) {
            Map<K, V> oldReference = this.reference.get();
            Map<K, V> copy = new HashMap<K, V>(oldReference);
            V previous = copy.put(key, value);
            if (reference.compareAndSet(oldReference, copy)) {
                return previous;
            }
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see java.util.Map#putAll(java.util.Map)
     */
    public void putAll(Map<? extends K, ? extends V> m) {
        while (true) {
            Map<K, V> oldReference = this.reference.get();
            Map<K, V> copy = new HashMap<K, V>(oldReference);
            copy.putAll(m);
            if (reference.compareAndSet(oldReference, copy)) {
                return;
            }
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see java.util.Map#remove(java.lang.Object)
     */
    public V remove(Object key) {
        while (true) {
            Map<K, V> oldReference = this.reference.get();
            Map<K, V> copy = new HashMap<K, V>(oldReference);
            V previous = copy.remove(key);
            if (reference.compareAndSet(oldReference, copy)) {
                return previous;
            }
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see java.util.Map#size()
     */
    public int size() {
        return this.reference.get().size();
    }

    /**
     * {@inheritDoc}
     * 
     * @see java.util.Map#values()
     */
    public Collection<V> values() {
        return Collections.unmodifiableCollection(this.reference.get().values());
    }

    /**
     * {@inheritDoc}
     */
    public V putIfAbsent(K key, V value) {
        while (true) {
            Map<K, V> oldReference = this.reference.get();
            if (oldReference.containsKey(key)) {
                return oldReference.get(key);
            }
            Map<K, V> copy = new HashMap<K, V>(oldReference);
            V previous = copy.put(key, value);
            if (reference.compareAndSet(oldReference, copy)) {
                return previous;
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public boolean remove(Object key, Object value) {
        while (true) {
            Map<K, V> oldReference = this.reference.get();
            V oldValue = oldReference.get(key);
            if (oldValue != null && oldValue.equals(value)) {
                Map<K, V> copy = new HashMap<K, V>(oldReference);
                copy.remove(key);
                if (reference.compareAndSet(oldReference, copy)) {
                    return true;
                }
            } else {
                return false;
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public V replace(K key, V value) {
        while (true) {
            Map<K, V> oldReference = this.reference.get();
            if (oldReference.containsKey(key)) {
                Map<K, V> copy = new HashMap<K, V>(oldReference);
                V previous = copy.put(key, value);
                if (reference.compareAndSet(oldReference, copy)) {
                    return previous;
                }
            } else {
                return null;
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public boolean replace(K key, V oldValue, V newValue) {
        while (true) {
            Map<K, V> oldReference = this.reference.get();
            V old = oldReference.get(key);
            if (old != null && old.equals(oldValue)) {
                Map<K, V> copy = new HashMap<K, V>(oldReference);
                copy.put(key, newValue);
                if (reference.compareAndSet(oldReference, copy)) {
                    return true;
                }
            } else {
                return false;
            }
        }
    }

}
